/* Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.activiti.workflow.simple.converter.step;

import org.activiti.bpmn.model.*;
import org.activiti.workflow.simple.converter.ConversionConstants;
import org.activiti.workflow.simple.converter.WorkflowDefinitionConversion;
import org.activiti.workflow.simple.definition.FeedbackStepDefinition;
import org.activiti.workflow.simple.definition.StepDefinition;
import org.activiti.workflow.simple.util.JvmUtil;

import java.util.*;


/**
 * @author Joram Barrez
 */
public class FeedbackStepDefinitionConverter extends BaseStepDefinitionConverter<FeedbackStepDefinition, Map<String, BaseElement>> {

    public static final String VARIABLE_FEEDBACK_PROVIDERS = "feedbackProviders";
    public static final String VARIABLE_FEEDBACK_PROVIDER = "feedbackProvider";
    private static final long serialVersionUID = 1L;
    private static final String SELECT_PEOPLE_USER_TASK = "initiatorSelectPeopleTask";
    private static final String FEEDBACK_FORK = "feedbackFork";
    private static final String FEEDBACK_JOIN = "feedbackJoin";
    private static final String FEEDBACK_USER_TASK = "gatherFeedback";

    @Override
    public Class<? extends StepDefinition> getHandledClass() {
        return FeedbackStepDefinition.class;
    }

    @Override
    protected Map<String, BaseElement> createProcessArtifact(FeedbackStepDefinition feedbackStepDefinition, WorkflowDefinitionConversion conversion) {

        // See feedback-step.png in the resource folder to get a graphical understanding of the conversion below

        Map<String, BaseElement> processElements = new HashMap<String, BaseElement>();

        // The first user task, responsible for configuring the feedback
        UserTask selectPeopleUserTask = createSelectPeopleUserTask(feedbackStepDefinition, conversion, processElements);

        // Parallel gateways (forking/joining)
        ParallelGateway fork = createForkParallelGateway(conversion, processElements);
        addSequenceFlow(conversion, selectPeopleUserTask, fork);

        // Gather feedback user task for the initiator of the feedback step
        UserTask gatherFeedbackUserTask = createGatherFeedbackUserTask(feedbackStepDefinition, conversion, processElements);
        addSequenceFlow(conversion, fork, gatherFeedbackUserTask);

        // Global signal event
        Signal signal = createSignalDeclaration(conversion);

        // Signal throw event after the gather feedback task
        ThrowEvent signalThrowEvent = createSignalThrow(conversion, signal);
        addSequenceFlow(conversion, gatherFeedbackUserTask, signalThrowEvent);

        // Povide feedback step
        UserTask feedbackTask = createFeedbackUserTask(feedbackStepDefinition, conversion, processElements);
        addSequenceFlow(conversion, fork, feedbackTask);

        // Boundary signal catch to shut down all tasks if the 'gather feedback' task is completed
        BoundaryEvent boundarySignalCatch = createBoundarySignalCatch(conversion, signal, feedbackTask);

        // Exclusive gateway after the feedback task, needed to correctly merge the sequence flow
        // such that the joining parallel gateway has exactly two incoming sequence flow
        ExclusiveGateway mergingExclusiveGateway = createMergingExclusiveGateway(conversion);
        addSequenceFlow(conversion, feedbackTask, mergingExclusiveGateway);
        addSequenceFlow(conversion, boundarySignalCatch, mergingExclusiveGateway);

        // Parallel gateway that will join  it all together
        ParallelGateway join = createJoinParallelGateway(conversion, processElements);
        addSequenceFlow(conversion, signalThrowEvent, join);
        addSequenceFlow(conversion, mergingExclusiveGateway, join);

        // Set the last activity id, such that next steps can connect correctly
        conversion.setLastActivityId(join.getId());

        return processElements;
    }

    protected UserTask createSelectPeopleUserTask(FeedbackStepDefinition feedbackStepDefinition, WorkflowDefinitionConversion conversion,
                                                  Map<String, BaseElement> processElements) {
        UserTask selectPeopleUserTask = new UserTask();
        selectPeopleUserTask.setId(conversion.getUniqueNumberedId(ConversionConstants.USER_TASK_ID_PREFIX));
        selectPeopleUserTask.setName(getSelectPeopleTaskName());
        selectPeopleUserTask.setAssignee(feedbackStepDefinition.getFeedbackInitiator());
        addFlowElement(conversion, selectPeopleUserTask, true);
        processElements.put(SELECT_PEOPLE_USER_TASK, selectPeopleUserTask);

        // TODO: work out form such that it can be used in Activiti Explorer, ie. add correct form properties
        // The following is just a a bit of a dummy form property
        FormProperty feedbackProvidersProperty = new FormProperty();
        feedbackProvidersProperty.setId(VARIABLE_FEEDBACK_PROVIDERS);
        feedbackProvidersProperty.setName("Who needs to provide feedback?");
        feedbackProvidersProperty.setRequired(true);
        feedbackProvidersProperty.setType("string"); // TODO: we need some kind of 'people' property type here

        selectPeopleUserTask.setFormProperties(Arrays.asList(feedbackProvidersProperty));

        // When the list of feedback providers is fixed up front, we need to add a script listener
        // that injects these variables into the process (instead of having it provided by the end user in a form)
        if (feedbackStepDefinition.getFeedbackProviders() != null && !feedbackStepDefinition.getFeedbackProviders()
                .isEmpty()) {
            if (selectPeopleUserTask.getTaskListeners() == null) {
                selectPeopleUserTask.setTaskListeners(new ArrayList<ActivitiListener>());
            }

            ActivitiListener taskListener = new ActivitiListener();
            taskListener.setEvent("complete");
            taskListener.setImplementationType(ImplementationType.IMPLEMENTATION_TYPE_CLASS);
            taskListener.setImplementation("org.activiti.engine.impl.bpmn.listener.ScriptTaskListener");

            FieldExtension languageField = new FieldExtension();
            languageField.setFieldName("language");
            languageField.setStringValue("JavaScript");

            FieldExtension scriptField = new FieldExtension();
            scriptField.setFieldName("script");

            StringBuilder script = new StringBuilder();
            if (JvmUtil.isJDK8()) {
                script.append("load(\"nashorn:mozilla_compat.js\");");
            }
            script.append("importPackage (java.util); var feedbackProviders = new ArrayList();" + System.getProperty("line.separator"));
            for (String feedbackProvider : feedbackStepDefinition.getFeedbackProviders()) {
                script.append("feedbackProviders.add('" + feedbackProvider + "');" + System.getProperty("line.separator"));
            }
            script.append("task.getExecution().setVariable('" + VARIABLE_FEEDBACK_PROVIDERS + "', feedbackProviders);" + System.getProperty("line.separator"));
            scriptField.setStringValue(script.toString());

            taskListener.setFieldExtensions(Arrays.asList(languageField, scriptField));

            selectPeopleUserTask.getTaskListeners().add(taskListener);
        }


        return selectPeopleUserTask;
    }

    protected ParallelGateway createForkParallelGateway(WorkflowDefinitionConversion conversion, Map<String, BaseElement> processElements) {
        ParallelGateway fork = new ParallelGateway();
        fork.setId(conversion.getUniqueNumberedId(ConversionConstants.GATEWAY_ID_PREFIX));
        addFlowElement(conversion, fork);
        processElements.put(FEEDBACK_FORK, fork);
        return fork;
    }

    protected UserTask createGatherFeedbackUserTask(FeedbackStepDefinition feedbackStepDefinition, WorkflowDefinitionConversion conversion,
                                                    Map<String, BaseElement> processElements) {
        UserTask gatherFeedbackUserTask = new UserTask();
        gatherFeedbackUserTask.setId(conversion.getUniqueNumberedId(ConversionConstants.USER_TASK_ID_PREFIX));
        gatherFeedbackUserTask.setName(getGatherFeedbackTaskName());
        gatherFeedbackUserTask.setAssignee(feedbackStepDefinition.getFeedbackInitiator());
        addFlowElement(conversion, gatherFeedbackUserTask);
        processElements.put(SELECT_PEOPLE_USER_TASK, gatherFeedbackUserTask);
        return gatherFeedbackUserTask;
    }

    protected Signal createSignalDeclaration(WorkflowDefinitionConversion conversion) {
        Signal signal = new Signal();
        String uniqueSignalId = "signal-" + UUID.randomUUID().toString();
        signal.setId(uniqueSignalId);
        signal.setName(uniqueSignalId);
        signal.setScope(Signal.SCOPE_PROCESS_INSTANCE);

        conversion.getBpmnModel().addSignal(signal);

        return signal;
    }

    protected ThrowEvent createSignalThrow(WorkflowDefinitionConversion conversion, Signal signal) {
        ThrowEvent signalThrowEvent = new ThrowEvent();
        signalThrowEvent.setId(conversion.getUniqueNumberedId(ConversionConstants.EVENT_ID_PREFIX));

        SignalEventDefinition signalThrowEventDefinition = new SignalEventDefinition();
        signalThrowEventDefinition.setSignalRef(signal.getId());
        signalThrowEvent.addEventDefinition(signalThrowEventDefinition);

        addFlowElement(conversion, signalThrowEvent);

        return signalThrowEvent;
    }

    protected ParallelGateway createJoinParallelGateway(WorkflowDefinitionConversion conversion, Map<String, BaseElement> processElements) {
        ParallelGateway join = new ParallelGateway();
        join.setId(conversion.getUniqueNumberedId(ConversionConstants.GATEWAY_ID_PREFIX));
        addFlowElement(conversion, join);
        processElements.put(FEEDBACK_JOIN, join);
        return join;
    }

    protected UserTask createFeedbackUserTask(FeedbackStepDefinition feedbackStepDefinition, WorkflowDefinitionConversion conversion,
                                              Map<String, BaseElement> processElements) {
        UserTask feedbackTask = new UserTask();
        feedbackTask.setId(conversion.getUniqueNumberedId(ConversionConstants.USER_TASK_ID_PREFIX));
        feedbackTask.setName(getProvideFeedbackTaskName());
        feedbackTask.setAssignee("${" + VARIABLE_FEEDBACK_PROVIDER + "}");

        MultiInstanceLoopCharacteristics multiInstanceLoopCharacteristics = new MultiInstanceLoopCharacteristics();
        multiInstanceLoopCharacteristics.setSequential(false);
        multiInstanceLoopCharacteristics.setInputDataItem(VARIABLE_FEEDBACK_PROVIDERS);
        multiInstanceLoopCharacteristics.setElementVariable(VARIABLE_FEEDBACK_PROVIDER);
        feedbackTask.setLoopCharacteristics(multiInstanceLoopCharacteristics);

        addFlowElement(conversion, feedbackTask);
        processElements.put(FEEDBACK_USER_TASK, feedbackTask);
        return feedbackTask;
    }

    protected BoundaryEvent createBoundarySignalCatch(WorkflowDefinitionConversion conversion, Signal signal, UserTask feedbackTask) {
        BoundaryEvent boundarySignalCatch = new BoundaryEvent();
        boundarySignalCatch.setId(conversion.getUniqueNumberedId(ConversionConstants.BOUNDARY_ID_PREFIX));
        boundarySignalCatch.setAttachedToRef(feedbackTask);
        boundarySignalCatch.setAttachedToRefId(feedbackTask.getId());
        boundarySignalCatch.setCancelActivity(true);
        addFlowElement(conversion, boundarySignalCatch);

        SignalEventDefinition signalCatchEventDefinition = new SignalEventDefinition();
        signalCatchEventDefinition.setSignalRef(signal.getId());
        boundarySignalCatch.addEventDefinition(signalCatchEventDefinition);
        return boundarySignalCatch;
    }

    protected ExclusiveGateway createMergingExclusiveGateway(WorkflowDefinitionConversion conversion) {
        ExclusiveGateway mergingExclusiveGateway = new ExclusiveGateway();
        mergingExclusiveGateway.setId(conversion.getUniqueNumberedId(ConversionConstants.GATEWAY_ID_PREFIX));
        addFlowElement(conversion, mergingExclusiveGateway);
        return mergingExclusiveGateway;
    }


    // The following are default task names and can be overidden by subclasses

    protected String getSelectPeopleTaskName() {
        return "Choose people";
    }

    protected String getProvideFeedbackTaskName() {
        return "Provide feedback";
    }

    protected String getGatherFeedbackTaskName() {
        return "Gather feedback";
    }

}
